package feevale.dropbox.service;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import feevale.dropbox.util.Protocol;

/**
 * 
 * @author richard
 *
 */
public class Share implements Runnable {

	private Thread thread;
	private Socket socket;
	private DataInputStream in;
	private DataOutputStream out;

	private String folder;

	/**
	 * Indica se irá mostrar mensagens de debug durante o processo
	 */
	private static boolean debugging = false;

	/**
	 * Tempo de espera entre cada laço para idenfiticar alterações no diretório.
	 */
	public static final Long TIME_SYNCHRONIZED = 1000L;

	/**
	 * Separador dos comandos utilizados no protocolo.
	 */
	private static final String SEPARATOR = "|";
	private static final String ESCAPE_SEPARATOR = "\\|";

	/**
	 * Tamanho default do buffer utilizado.
	 */
	private static final Integer BUFFER_SIZE = 1024;

	/**
	 * Lista de arquivos. Utilizar para armazenar as estruturas de arquivos do
	 * diretório. Caso tenha alguma alteração, é necessário enviar comando para
	 * o servidor.
	 */
	public static List<File> FILES = new ArrayList<File>();

	public Share(Socket socket, String folder) {

		try {
			this.socket = socket;
			this.folder = folder;

			this.in = new DataInputStream(this.socket.getInputStream());
			this.out = new DataOutputStream(this.socket.getOutputStream());

			this.thread = new Thread(this);

			setup();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * 
	 */
	@Override
	public void run() {
		
		int n = 0;
		byte[] buffer = new byte[BUFFER_SIZE];
		String[] parts;
		
		while (true) {
			
			debug("SETUP: " + FILES.size());
			for (File f : FILES) {
				debug(f.getName());
			}
			debug("_____________________");
			
			try {
				out.write(Protocol.LIST.getKey().concat(SEPARATOR).getBytes());
				out.flush();
				
				List<File> localDeletedFiles = new ArrayList<File>();
				
				debug("aguardando LDF");
				
				// aguarda a lista de arquivos deletados
				while ((n = in.read(buffer)) > -1) {
					
					parts = getCommandParts(new String(buffer, 0, n));
					
					if (!(parts[0].equals(Protocol.LIST_DELETED_FILES.getKey()))) {
						continue;
					}
					
					// atribui lista de arquivos deltados localmente
					localDeletedFiles = getLocalDeletedFiles();
					
					for (String fileName : getDeletedFileNames(parts)) {
						File file = getFileByName(fileName);
						
						if (file.exists()) {
							System.out.println("REMOVENDO O ARQUIVO: " + fileName);
							
							file.delete();
						}						
					}
					
					// envia a confirmação de exclusão dos arquivos deletados no servidor
					out.write(Protocol.LIST_DELETED_CONFIRMATION.getValue());
					out.flush();
					
					break;
				}
				
				//aguarda mensagem com a lista de arquivos existentes no servidor
				n = 0;
				buffer = new byte[BUFFER_SIZE];
				
				debug("aguardando LEF");
				
				while ((n = in.read(buffer)) > -1) {
					parts = getCommandParts(new String(buffer, 0, n));
					
					if (!(parts[0].equals(Protocol.LIST_EXISTENT_FILES.getKey()))) {
						continue;
					}
					
					List<String> onlyServerFiles = getOnlyServerFiles(parts);
					List<File> onlyClientFiles = getOnlyClientFiles(parts);
					
					debug("SERVER FILES SIZE: " + onlyServerFiles.size());
					
					// envia a confirmacao do recebimento da mensagem
					out.write(Protocol.LIST_EXISTENT_CONFIRMATION.getValue());
					out.flush();
					
					// download arquivos do servidor
					for (String fileServerName : onlyServerFiles) {
						
						File file = new File(this.folder + File.separator + fileServerName);
						
						if (!(localDeletedFiles.contains(file))) {
							System.out.println("DOWNLOAD DO ARQUIVO: " + fileServerName);
							
							debug(getDownloadCommand(fileServerName));
							
							out.write(getDownloadCommand(fileServerName).getBytes());
							out.flush();
							
							n = 0;
							buffer = new byte[BUFFER_SIZE];
							
							while ((n = in.read(buffer)) > -1) {
								parts = getCommandParts(new String(buffer, 0, n));
								
								if (!(parts[0].equals(Protocol.UPLOAD.getKey()))) {
									continue;
								}
								
								FileOutputStream fOut = new FileOutputStream(new File(this.folder + File.separator + parts[2]));
								
								n = 0;
								buffer = new byte[BUFFER_SIZE];
								
								while (fOut.getChannel().size() < Long.parseLong(parts[3])) {
									n = in.read(buffer);
									fOut.write(buffer, 0, n);
								}
								
								fOut.close();
								
								break;
							}
							
							debug("Add in FILES");
							FILES.add(new File(this.folder + File.separator + parts[2]));
							
						} else {
							System.out.println("REMOVENDO O ARQUIVO: " + fileServerName);
							
							out.write(getDeletedFileCommand(fileServerName).getBytes());
							out.flush();
							
							n = 0;
							buffer = new byte[BUFFER_SIZE];
							
							// agurda a confirmacao da exclusao do arquivo
							while ((n = in.read(buffer)) > -1) {
								debug("delete confirmation");
								
								parts = getCommandParts(new String(buffer, 0, n));
								
								if (parts[0].equals(Protocol.DELETE_CONFIRMATION.getKey())) {
									debug("RETIRANDO DA LISTA ####");
									FILES.remove(getFileByName(fileServerName));
									
									break;
								}
							}
						}
						
						debug("TTTT");
					}
					
					debug("enviando arquivos para o servidor");
					debug(String.valueOf(onlyClientFiles.size()));
					
					// envio dos arquivos somente locais
					for (File uploadFile : onlyClientFiles) {
						debug(getUploadCommand(uploadFile));
						
						if (uploadFile.exists()) {
							System.out.println("UPLOAD DO ARQUIVO: " + uploadFile.getName());
							
							out.write(getUploadCommand(uploadFile).getBytes());
							out.flush();
							
							FileInputStream fIn = new FileInputStream(uploadFile);
							
							n = 0;
							buffer = new byte[BUFFER_SIZE];
							
							int outSize = 0;
							
							while (outSize < uploadFile.length()) {
								debug("entrou no while client");
								
								n = fIn.read(buffer);
								out.write(buffer, 0, n);
								out.flush();
								
								outSize += n;
							}
							
							debug("close client");
							
							fIn.close();
							
							// adiciona a lista local (memória)
							if (!(FILES.contains(uploadFile))) {
								FILES.add(uploadFile);
							}
							
							// aguarda confirmação de upload.
							n = 0;
							buffer = new byte[BUFFER_SIZE];
							
							while ((n = in.read(buffer)) > -1) {
								parts = getCommandParts(new String(buffer, 0, n));
								
								if (parts[0].equals(Protocol.UPLOAD_CONFIRMATION.getKey())) {
									break;
								}
							}
						}
					}
					
					break;
				}
				
				debug("antes da thread");
				
				// Quando terminar o fluxo !
				Thread.sleep(TIME_SYNCHRONIZED);
				
			} catch (IOException e) {
				e.printStackTrace();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	private File getFileByName(String fileName) {
		File file = new File(this.folder + File.separator + fileName);
		return file;
	}
	
	private static String getDeletedFileCommand(String fileName) {
		StringBuilder builder = new StringBuilder();
		builder.append(Protocol.DELETE.getKey());
		builder.append(SEPARATOR);
		builder.append(formatFileNameSize(fileName.length()));
		builder.append(SEPARATOR);
		builder.append(fileName);
		builder.append(SEPARATOR);
		
		return builder.toString();
	}
	
	private static String getUploadCommand(File file) {
		StringBuilder builder = new StringBuilder();
		builder.append(Protocol.UPLOAD.getKey());
		builder.append(SEPARATOR);
		builder.append(formatFileNameSize(file.getName().length()));
		builder.append(SEPARATOR);
		builder.append(file.getName());
		builder.append(SEPARATOR);
		builder.append(file.length());
		builder.append(SEPARATOR);

		return builder.toString();
	}
	
	private static String getDownloadCommand(String fileName) {
		StringBuilder builder = new StringBuilder();
		builder.append(Protocol.DOWNLOAD.getKey());
		builder.append(SEPARATOR);
		builder.append(formatFileNameSize(fileName.length()));
		builder.append(SEPARATOR);
		builder.append(fileName);
		builder.append(SEPARATOR);

		return builder.toString();
	}

	private static String formatFileNameSize(Integer size) {
		return String.format("%04d", size);
	}

	private List<String> getOnlyServerFiles(String[] command) {
		List<String> filesNames = new ArrayList<String>();

		for (int i = 2; i < command.length; i += 2) {
			File f = new File(this.folder + File.separator + command[i]);

			if (!(f.exists())) {
				filesNames.add(command[i]);
				debug(command[i]);
			}
		}

		return filesNames;
	}

	private List<File> getOnlyClientFiles(String[] command) {
		List<File> files = new ArrayList<File>();
		List<String> filesNames = new ArrayList<String>();
		
		debug("------------------");
		debug("ONLY CLIENT");
		for (String string : command) {
			debug(string);
		}
		debug("------------------");
		
		for (int i = 2; i < command.length; i += 2) {
			filesNames.add(command[i]);
		}
		
		debug("!!! filesNames");
		for (String string : filesNames) {
			debug(string);
		}
		
		debug("$$$$$$");
		debug("FOLDER FILES");
		for (File file : getFolderFiles()) {
			debug(file.getName());
		}
		debug("$$$$$$");
		
		for (File file : getFolderFiles()) {
			if (!(filesNames.contains(file.getName()))) {
				debug("ADD FILE: " + file.getName());
				files.add(file);
			}
		}
		
		debug("%%%%%  ONLY CLIENT FILES SIZE: " + files.size());
		
		return files;
	}

	private List<File> getLocalDeletedFiles() {
		List<File> deletedFiles = new ArrayList<File>();
		
		for (File file : FILES) {
			if (!file.exists()) {
				deletedFiles.add(file);
			}
		}
		
		debug("DELETED FILES: " + deletedFiles.size());
		
		return deletedFiles;
	}

	private String[] getCommandParts(String txt) {
		String[] parts = txt.split(ESCAPE_SEPARATOR);
		return parts;
	}

	private List<String> getDeletedFileNames(String[] command) {
		List<String> fileNames = new ArrayList<String>();

		for (int i = 2; i < command.length; i += 2) {
			fileNames.add(command[i]);
		}
		
		return fileNames;
	}

	public void exit() {
		try {
			finalize();
		} catch (Throwable e) {
			e.printStackTrace();
		}
	}

	/**
	 * Método responsável por realizar o setup inicial dos arquivos contidos no
	 * diretório com a estrutura de controle do cliente.
	 */
	private void setup() {
		
		for (File file : getFolderFiles()) {
			if (!FILES.contains(file)) {
				FILES.add(file);
			}
		}

//		new DirectoryMonitor(this);
		
		this.thread.start();
	}

	/**
	 * Retorna todos os arquivos contidos no diretório.
	 * 
	 * @return
	 */
	private List<File> getFolderFiles() {
		List<File> files = new ArrayList<File>();

		for (File file : getFolder().listFiles()) {
			if (file.isFile()) {
				files.add(file);
			}
		}

		return files;
	}

	/**
	 * Retorna o folder a partir da configuração do seu caminho.
	 * 
	 * @return
	 */
	public File getFolder() {
		File folder = new File(this.folder);
		return folder;
	}

	private static void debug(String log) {
		if (debugging) {
			System.out.println(new SimpleDateFormat("hh:mm:ss.SSS").format(new Date()) + " - " + log);
		}
	}

}
